﻿using System;
using System.Collections.Generic;

namespace Tuxedo
{
	internal class TokenStream
	{
		private Token[] tokens;
		private int currentToken = 0;
		private int tokenCount;

		private enum State
		{
			Normal,
			String,
			CharString,
			RawString,
			LineComment,
			MultiLineComment,
			Word
		}

		private int[][] GenerateLineAndColLookupByIndex(string text)
		{
			int line = 1;
			int col = 1;
			int length = text.Length;
			List<int> lines = new List<int>();
			List<int> cols = new List<int>();
			char c;
			for (int i = 0; i < length; ++i)
			{
				c = text[i];
				lines.Add(line);
				cols.Add(col);
				if (c == '\n')
				{
					++line;
					col = 1;
				}
				else
				{
					++col;
				}
			}

			return new int[][] { lines.ToArray(), cols.ToArray() };
		}

		private static readonly HashSet<string> TWO_CHAR_TOKENS = new HashSet<string>("++ -- >= <= != ?? << == || && &= |= ^= += -= %= *= /=".Split(' '));
		private static readonly HashSet<string> THREE_CHAR_TOKENS = new HashSet<string>("<<= >>=".Split(' '));

		private bool IsValidIdentifierChar(char c)
		{
			if (c >= 'a' && c <= 'z') return true;
			if (c >= 'A' && c <= 'Z') return true;
			if (c >= '0' && c <= '9') return true;
			if (c == '_') return true;
			// TODO: valid unicode character?
			return false;
		}

		public TokenStream(CsProjData project, string fileName, string fileContents)
		{
			int[][] lookups = this.GenerateLineAndColLookupByIndex(fileContents);
			int[] lineLookup = lookups[0];
			int[] columnLookup = lookups[1];
			string[] lines = fileContents.Split('\n');
			List<Token> tokens = new List<Token>();

			int length = fileContents.Length;
			char c;
			string c2;
			string c3;
			State state = State.Normal;
			string currentToken = "";
			int tokenStart = -1;

			for (int i = 0; i < length; ++i)
			{
				c = fileContents[i];
				c2 = (i < length - 1) ? "" + c + fileContents[i + 1] : null;
				c3 = (i < length - 2) ? "" + c + fileContents[i + 1] + fileContents[i + 2] : null;

				switch (state)
				{
					case State.Normal:
						if (c == ' ' || c == '\t' || c == '\n' || c == '\r')
						{
							// do nothing. skip whitespace.
						}
						else if (c == '"')
						{
							currentToken = "\"";
							tokenStart = i;
							state = State.String;
						}
						else if (c == '\'')
						{
							currentToken = "'";
							tokenStart = i;
							state = State.CharString;
						}
						else if (c2 == "/*")
						{
							state = State.MultiLineComment;
							++i;
						}
						else if (c2 == "//")
						{
							state = State.LineComment;
						}
						else if (c2 == "@\"")
						{
							state = State.RawString;
							tokens.Add(new Token("@", lineLookup[i], columnLookup[i], fileName, lines[lineLookup[i] - 1], project));
							++i;
							currentToken = "\"";
							tokenStart = i;
						}
						else if (this.IsValidIdentifierChar(c))
						{
							// TODO: C# allows unicode identifier names.
							currentToken = "" + c;
							tokenStart = i;
							state = State.Word;
						}
						else if (THREE_CHAR_TOKENS.Contains(c3))
						{
							tokens.Add(new Token(c3, lineLookup[i], columnLookup[i], fileName, lines[lineLookup[i] - 1], project));
							i += 2;
						}
						else if (TWO_CHAR_TOKENS.Contains(c2))
						{
							tokens.Add(new Token(c2, lineLookup[i], columnLookup[i], fileName, lines[lineLookup[i] - 1], project));
							++i;
						}
						else // some sort of punctuation
						{
							Token token = new Token("" + c, lineLookup[i], columnLookup[i], fileName, lines[lineLookup[i] - 1], project);
							tokens.Add(token);

							// Hack for >> 
							// Because it's possible to have types with names like List<List<int>> where >> appears but should be considered
							// 2 separate > tokens, I have to check for bitshift as two consecutive > tokens.
							// However I do not want 2 >'s with any sort of whitespace between them to get counted as a false positive.
							if (i > 0 && fileContents[i - 1] == '>')
							{
								token.PreviousIsGreaterThanCharacterHACK = true;
							}
						}
						break;

					case State.CharString:
					case State.String:
						bool charString = state == State.CharString;
						if (c == '\n' || c == '\r')
						{
							throw new Exception("Unexpected new line");
						}
						else if (c == '\\')
						{
							if (c2 == null)
							{
								throw new Exception("Unexpected EOF");
							}
							else
							{
								currentToken += c2;
								++i;
							}
						}
						else if ((charString && c == '\'') || (!charString && c == '"'))
						{
							currentToken += charString ? "'" : "\"";
							state = State.Normal;
							tokens.Add(new Token(currentToken, lineLookup[i], columnLookup[i], fileName, lines[lineLookup[i] - 1], project));
						}
						else
						{
							currentToken += c;
						}
						break;

					case State.RawString:
						if (c == '"')
						{
							currentToken += '"';
							state = State.Normal;
							tokens.Add(new Token(currentToken, lineLookup[tokenStart], columnLookup[tokenStart], fileName, lines[lineLookup[tokenStart] - 1], project));
						}
						else
						{
							currentToken += c;
						}
						break;

					case State.LineComment:
						if (c == '\n')
						{
							state = State.Normal;
						}
						break;

					case State.MultiLineComment:
						if (c2 == "*/")
						{
							state = State.Normal;
							++i;
						}
						break;

					case State.Word:
						if (this.IsValidIdentifierChar(c))
						{
							currentToken += c;
						}
						else
						{
							tokens.Add(new Token(currentToken, lineLookup[tokenStart], columnLookup[tokenStart], fileName, lines[lineLookup[tokenStart] - 1], project));
							--i;
							state = State.Normal;
						}
						break;

					default:
						throw new Exception("WAT?");
				}
			}

			if (state == State.Word)
			{
				tokens.Add(new Token(currentToken, lineLookup[tokenStart], columnLookup[tokenStart], fileName, lines[lineLookup[tokenStart] - 1], project));
			}
			else if (state == State.Normal ||
				state == State.LineComment) { }
			else
			{
				throw new Exception("Unexpected EOF");
			}

			this.tokens = tokens.ToArray();
			this.tokenCount = this.tokens.Length;
		}

		public Token Peek()
		{
			if (this.currentToken < this.tokenCount)
			{
				return this.tokens[this.currentToken];
			}
			return null;
		}

		public string PeekValue()
		{
			if (this.currentToken < this.tokenCount)
			{
				return this.tokens[this.currentToken].Value;
			}
			return null;
		}

		public Token Pop()
		{
			if (this.currentToken < this.tokenCount)
			{
				++this.currentToken;
				return this.tokens[this.currentToken - 1];
			}
			return null;
		}

		public bool PopIfPresent(string value)
		{
			if (this.currentToken < this.tokenCount)
			{
				if (this.tokens[this.currentToken].Value == value)
				{
					++this.currentToken;
					return true;
				}
			}
			return false;
		}

		public void PopExpected(string value)
		{
			if (!this.PopIfPresent(value))
			{
				// TODO: more descriptive error message
				throw new Exception("Unexpected token");
			}
		}

		public string PopAnyOf(params string[] values)
		{
			foreach (string value in values)
			{
				if (this.PopIfPresent(value))
				{
					return value;
				}
			}
			return null;
		}

		public enum NumberType
		{
			Decimal,
			Hex,
			Octal,
			Float
		}

		public static bool IsNumber(string value)
		{
			if (value.Length > 3 && value[0] == '0' && value[1] == 'x')
			{
				for (int i = 2; i < value.Length; ++i)
				{
					char c = value[i];
					if ((c >= '0' && c <= '9') ||
						(c >= 'A' && c <= 'F') ||
						(c >= 'a' && c <= 'f'))
					{
						// okidoke
					}
					else
					{
						return false;
					}
				}
				return true;
			}

			if (value[0] == '0')
			{
				for (int i = 1; i < value.Length; ++i)
				{
					char c = value[i];
					if (c < '0' || c > '7')
					{
						// not necessary? 09 seems valid
						return false;
					}
				}
			}
			else
			{
				for (int i = 0; i < value.Length; ++i)
				{
					char c = value[i];
					if (c < '0' || c > '9')
					{
						return false;
					}
				}
			}
			return true;
		}

		public Token PopWord(bool failSilently)
		{
			if (this.currentToken < this.tokenCount)
			{
				Token token = this.tokens[this.currentToken];
				string value = token.Value;
				for (int i = 0; i < value.Length; ++i)
				{
					if (!this.IsValidIdentifierChar(value[i]))
					{
						if (failSilently) return null;
						throw new Exception("Expected identifier");
					}
				}
				++this.currentToken;

				return token;
			}
			if (failSilently) return null;
			throw new Exception("Expected identifier");
		}

		private static readonly HashSet<string> C_SHARP_KEYWORDS = new HashSet<string>((
			"abstract as break case catch checked class const continue " +
			"default delegate do else enum event explicit extern finally " +
			"fixed for foreach goto if implicit in interface internal is lock " +
			"namespace new operator out override params private protected public " +
			"readonly ref return sealed sizeof stackalloc static struct " +
			"switch throw try typeof unchecked unsafe using virtual " +
			"volatile while").Split(' '));

		public string PopIdentifier()
		{
			return this.PopIdentifier(false);
		}

		public string PopIdentifier(bool failSilently)
		{
			Token token = this.PopWord(failSilently);

			if (token == null)
			{
				if (failSilently) return null;
				throw new Exception("Identifier expected. EOF found instead.");
			}

			string value = token.Value;
			if (C_SHARP_KEYWORDS.Contains(value))
			{
				if (failSilently) return null;
				throw new Exception("Identifier expected; '" + value + "' is a keyword");
			}

			char c = value[0];
			// TODO: unicode is allowed
			if (c == '_' ||
				(c >= 'a' && c <= 'z') ||
				(c >= 'A' && c <= 'Z'))
			{
				return value;
			}

			if (failSilently) return null;
			throw new Exception("Unexpected token");
		}

		public bool HasMore()
		{
			return this.currentToken < this.tokenCount;
		}

		public bool IsNext(string value)
		{
			if (this.currentToken < this.tokenCount)
			{
				if (this.tokens[this.currentToken].Value == value)
				{
					return true;
				}
			}
			return false;
		}

		private Stack<int> states = new Stack<int>();

		public int GetCurrentIndex()
		{
			return this.currentToken;
		}

		public void RestoreIndex(int value)
		{
			this.currentToken = value;
		}
	}
}
